<?php

/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2019/3/12 0012
 * Time: 22:18
 */
////原文：https://blog.csdn.net/qq_33344121/article/details/79968775
///
/// https://blog.csdn.net/hguisu/article/details/7448528#t1

//socket_accept() 接受一个Socket连接
//socket_bind() 把socket绑定在一个IP地址和端口上
//socket_clear_error() 清除socket的错误或最后的错误代码
//socket_close() 关闭一个socket资源
//socket_connect() 开始一个socket连接
//socket_create_listen() 在指定端口打开一个socket监听
//socket_create_pair() 产生一对没有差别的socket到一个数组里
//socket_create() 产生一个socket，相当于产生一个socket的数据结构
//socket_get_option() 获取socket选项
//socket_getpeername() 获取远程类似主机的ip地址
//socket_getsockname() 获取本地socket的ip地址
//socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组
//socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构
//socket_iovec_delete() 删除一个已分配的iovec
//socket_iovec_fetch() 返回指定的iovec资源的数据
//socket_iovec_free() 释放一个iovec资源
//socket_iovec_set() 设置iovec的数据新值
//socket_last_error() 获取当前socket的最后错误代码
//socket_listen() 监听由指定socket的所有连接
//socket_read() 读取指定长度的数据
//socket_readv() 读取从分散/聚合数组过来的数据
//socket_recv() 从socket里结束数据到缓存   从已连接的socket接收数据
//socket_recvfrom() 接受数据从指定的socket，如果没有指定则默认当前socket
//socket_recvmsg() 从iovec里接受消息
//socket_select() 多路选择
//socket_send() 这个函数发送数据到已连接的socket
//socket_sendmsg() 发送消息到socket
//socket_sendto() 发送消息到指定地址的socket
//socket_set_block() 在socket里设置为块模式
//socket_set_nonblock() socket里设置为非块模式
//socket_set_option() 设置socket选项
//socket_shutdown() 这个函数允许你关闭读、写、或指定的socket
//socket_strerror() 返回指定错误号的周详错误
//socket_write() 写数据到socket缓存
//socket_writev() 写数据到分散/聚合数组

class webSocketServer
{
    private $sockets;//所有socket连接池包括服务端socket
    private $users; //所有连接用户
    private $server;//服务端socket

    public function __construct($ip, $port)
    {
        $this->server = socket_create(AF_INET, SOCK_STREAM, IPPROTO_IP) or die('socket_create fail' . socket_strerror(socket_last_error()));
        $this->sockets = [$this->server];
        $this->users = array();
        //绑定到socket
        socket_bind($this->server, $ip, $port) or die('socket_bind fail' . socket_strerror(socket_last_error()));
        //开始监听
        socket_listen($this->server, 3) or die('socket_listen fail' . socket_strerror(socket_last_error()));
        echo "[*]Listening:" . $ip . ":" . $port . "\n";
    }

    public function run()
    {
        $write = null;
        $except = null;
        while (true) {
            $active_sockets = $this->sockets;
// read读取数组中列出的套接字将被监视以查看字符是否可用于读取(更准确地说，以查看读取是否不会阻塞-特别是，在文件结束时也准备好了套接字资源，在这种情况下，Socket_read()将返回一个零长度字符串)。
// write将监视写数组中列出的套接字，以查看写入是否会阻塞。
// except除了数组中列出的套接字将被监视异常
// tv_sec  tv_sec、tv_sec和tv_usc共同构成超时参数。超时是Socket_SELECT()返回之前经过的时间的上限。tv_sec可能为零，
//从而导致Socket_select()立即返回。这对于轮询很有用。如果tv_sec为空(没有超时)，则Socket_SELECT()可以无限期地阻塞。

            socket_select($active_sockets, $write, $except, null);

//前三个参数时传入的是数组的引用,会依次从传入的数组中选择出可读的,可写的,异常的socket,我们只需要选择出可读的socket
//最后一个参数tv_sec很重要
//第一，若将NULL以形参传入，即不传入时间结构，就是将select置于阻塞状态，一定等到监视文件描述符集合(socket数组)中某个文件描
//述符发生变化为止；
//第二，若将时间值设为0秒0毫秒，就变成一个纯粹的非阻塞函数，不管文件描述符是否有变化，都立刻返回继续执行，文件无
//变化返回0，有变化返回一个正值；
//第三，timeout的值大于0，这就是等待的超时时间，即 select在timeout时间内阻塞，超时时间之内有事件到来就返回了，
//否则在超时后不管怎样一定返回，返回值同上述。
            foreach ($active_sockets as $active_socket) {

                if ($active_socket == $this->server) {
                    echo "Waiting for Connection...\n";
                    //服务端socket可读说明有新的客户端连接
                    $user_socket = socket_accept($this->server) or die('socket_accept fail' . socket_strerror(socket_last_error())); //成功时返回新套接字资源，错误时返回false
                    $key = uniqid();
                    $this->sockets[] = $user_socket;
                    $this->users[$key] = [
                        'socket' => $user_socket,
                        'handshake' => false //是否完成webSocket握手
                    ];
                } else {
                    //用户socket可读
                    $buffer = '';
                    $bytes = socket_recv($active_socket, $buffer, 1024, 0);//socket_recv() 返回一个数字，表示接收到的字节数。如果发生了错误，则返回 FALSE
                    $key = $this->findUserBySocket($active_socket); //通过socket在users数组中找出user
                    if ($bytes == 0) {
                        //没有数据 关闭连接
                        $this->close($active_socket);
                    } else {
                        //没有握手就先握手
                        if ($this->users[$key]['handshake'] == false) {
                            $this->handshake($key, $buffer);
                        } else {
                            //握手成功后解析消息 websocket协议有自己的消息格式
                            //解码和编码的过程是固定的
                            $msg = $this->msgDecode($buffer);
                            echo '收到客户端的数据：' . $msg;
                            //编码后发送回去
//                            $msg = "WELCOME \n";
                            $res_msg = $this->msgEncode($msg);
                            socket_write($active_socket, $res_msg, strlen($res_msg));
                        }
                    }
                }
            }

        }
    }

    //这个方法没用
    private function code($msg)
    {
        $msg = preg_replace(array('/\r$/', '/\n$/', '/\r\n$/',), '', $msg);
        $frame = array();
        $frame[0] = '81';
        $len = strlen($msg);
        $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
        $frame[2] = $this->ord_hex($msg);
        $data = implode('', $frame);
        return pack("H*", $data);
    }

    private function ord_hex($data)
    {
        $msg = '';
        $l = strlen($data);
        for ($i = 0; $i < $l; $i++) {
            $msg .= dechex(ord($data{$i}));
        }
        return $msg;
    }

    //通过socket在users数组中找出user
    private function findUserBySocket($socket)
    {
        foreach ($this->users as $key => $user) {
            if ($socket == $user['socket']) {
                return $key;
            }
        }
        return -1;
    }

    //解除连接
    private function close($socket)
    {
        $key = $this->findUserBySocket($socket);
        unset($this->users[$key]);
        foreach ($this->sockets as $k => $v) {
            if ($v == $socket)
                unset($this->sockets[$k]);
        }
        //socket_shutdown() 这个函数允许你关闭读、写、或指定的socket
        socket_shutdown($socket);
        socket_close($socket);
    }

    private function handshake($userSocketKey, $buffer)
    {
        //截取Sec-WebSocket-Key的值并加密
        $buf = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
        $key = trim(substr($buf, 0, strpos($buf, "\r\n")));
        $new_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); //Kfh9QIsMVZcl6xEPYxPHzW8SZ8w=
        //按照协议组合信息进行返回
        $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $new_message .= "Upgrade: websocket\r\n";
        $new_message .= "Sec-WebSocket-Version: 13\r\n";
        $new_message .= "Connection: Upgrade\r\n";
        $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
        socket_write($this->users[$userSocketKey]['socket'], $new_message, strlen($new_message)) or die('socket_write 失败原因是' . socket_strerror(socket_last_error()) . "\n");
        //对已经握手的client做标志
        $this->users[$userSocketKey]['handshake'] = true;
        return true;
    }

    //编码 把消息打包成websocket协议支持的格式
    private function msgEncode($buffer)
    {
        $len = strlen($buffer);
        if ($len <= 125) {
            return "\x81" . chr($len) . $buffer;
        } else if ($len <= 65535) {
            return "\x81" . chr(126) . pack("n", $len) . $buffer;
        } else {
            return "\x81" . char(127) . pack("xxxxN", $len) . $buffer;
        }
    }

    //解码 解析websocket数据帧
    private function msgDecode($buffer)
    {
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127) {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }
}

//$buf  = substr('',strpos('','Sec-WebSocket-Key:')+18);
//echo $buf;
//$key  = trim(substr($buf,0,strpos($buf,"\r\n")));
//echo $key;
//$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
//echo $new_key;
$ws = new WebSocketServer('0.0.0.0', 9502);
$ws->run();